﻿using DynORM.Enums;
using DynORM.Interfaces;
using DynORM.Interfaces.BLLMapper.Enum;
using DynORM.Mappers;
using DynORM.ReferenceObjects;
using DynORM.TypeMappingSubsystem;
using DynORM.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace DynORM.BLLMapper.Helpers
{
    public class TypeAndRowDescriptorAux
    {
        private static TypeAndRowDescriptorAux instance_mssql;
        private static TypeAndRowDescriptorAux instance_postgresql;
        private static TypeAndRowDescriptorAux instance_mysql;
        private static TypeAndRowDescriptorAux instance_oracle;

        public static TypeAndRowDescriptorAux GetInstance(DBMSTypes type)
        {
            if (type == DBMSTypes.MSSQL)
            {
                if (instance_mssql == null)
                    instance_mssql = new TypeAndRowDescriptorAux(type);
                return instance_mssql;
            }
            else if (type == DBMSTypes.PostgreSQL)
            {
                if (instance_postgresql == null)
                    instance_postgresql = new TypeAndRowDescriptorAux(type);
                return instance_postgresql;
            }
            else if (type == DBMSTypes.MySQL)
            {
                if (instance_mysql == null)
                    instance_mysql = new TypeAndRowDescriptorAux(type);
                return instance_mysql;
            }
            else if (type == DBMSTypes.Oracle)
            {
                if (instance_oracle == null)
                    instance_oracle = new TypeAndRowDescriptorAux(type);
                return instance_oracle;
            }
            else
                throw new Exception("DBMS type is not supported.");
        }

        private DBMSTypes dbmsType;

        private TypeAndRowDescriptorAux(DBMSTypes dbmsType)
        {
            if (dbmsType == DBMSTypes.MSSQL || dbmsType == DBMSTypes.PostgreSQL || dbmsType == DBMSTypes.MySQL || dbmsType == DBMSTypes.Oracle)
                this.dbmsType = dbmsType;
            else
                throw new Exception("DBMS type is not supported.");
        }



        public static IDictionary<int, MemberInfo> GetBLLObjTypePrimaryKeyMemberInfos(Type bllObjType)
        {
            Dictionary<int, MemberInfo> result = new Dictionary<int, MemberInfo>();

            IEnumerable<FieldInfo> fieldInfos = bllObjType.GetFields(BindingFlags.Public | BindingFlags.Instance);
            fieldInfos = fieldInfos.Where(fi => fi.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).Count() > 0);
            foreach (FieldInfo fi in fieldInfos)
            {
                PrimaryKeyAttribute pka = fi.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).Select<object, PrimaryKeyAttribute>(o => (PrimaryKeyAttribute)o).First();
                result.Add(pka.Ord, fi);
            }

            bool pkctor = false;
            foreach (ConstructorInfo ctor in bllObjType.GetConstructors())
                if (ctor.GetCustomAttributes(typeof(PrimaryConstructorAttribute), true).Count() == 1)
                    pkctor = true;

            IEnumerable<PropertyInfo> propertyInfos = bllObjType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            propertyInfos = propertyInfos.Where(pi => pi.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).Count() > 0);
            foreach (PropertyInfo pi in propertyInfos)
                if (pi.CanRead && (pi.CanWrite || pkctor))
                {
                    PrimaryKeyAttribute pka = pi.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).Select<object, PrimaryKeyAttribute>(o => (PrimaryKeyAttribute)o).First();
                    result.Add(pka.Ord, pi);
                }
            return result;
        }

        public static IEnumerable<MemberInfo> GetBLLObjTypeFieldsMemberInfos(Type bllObjType)
        {
            List<MemberInfo> result = new List<MemberInfo>();

            IEnumerable<FieldInfo> fieldInfos = bllObjType.GetFields(BindingFlags.Public | BindingFlags.Instance);
            fieldInfos = fieldInfos.Where(fi => fi.GetCustomAttributes(typeof(FieldAttribute), true).Count() > 0);
            foreach (FieldInfo fi in fieldInfos)
                result.Add(fi);

            IEnumerable<PropertyInfo> propertyInfos = bllObjType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            propertyInfos = propertyInfos.Where(pi => pi.GetCustomAttributes(typeof(FieldAttribute), true).Count() > 0);
            foreach (PropertyInfo pi in propertyInfos)
                if (pi.CanRead && pi.CanWrite)
                    result.Add(pi);
            return result;
        }

        public static Type GetCLRNullableType(Type type)
        {
            return Types.GetCLRNullableType(type);
        }

        public static string GetBLLObjTypeTableName(Type bllObjType)
        {
            string result = String.Empty;
            object[] cta = bllObjType.GetCustomAttributes(typeof(TableAttribute), false);
            if (cta.Count() > 0 && cta[0] != null)
                result = ((TableAttribute)cta[0]).TableName;
            else
                result = bllObjType.Name;

            return result;
        }



        public bool IsExistsInDB(Type bllObjType, string connectionString)
        {
            IRowDescriptor rd = TypeAndRowDescriptorAux.GetInstance(dbmsType).GetBLLObjTypeRowDescriptor(bllObjType);
            return RowDescriptorMapper.IsExistsInDB(rd, connectionString, dbmsType);
        }

        public bool IsResolvebleToDBMSType(Type type)
        {
            string DBMSTypeName;
            try
            {
                DBMSTypeName = Types.GetInstance(this.dbmsType).CLRType2DBTypeName(type);
                if (DBMSTypeName != String.Empty)
                    return true;
                else
                    return false;
            }
            catch
            {
                return false;
            }
        }

        public bool IsResolvebleToCLRType(string dbTypeName, bool isNullable)
        {
            Type CLRType;
            try
            {
                CLRType = Types.GetInstance(dbmsType).DBTypeName2CLRType(dbTypeName, isNullable);
                if (CLRType != null)
                    return true;
                else
                    return false;
            }
            catch
            {
                return false;
            }
        }

        public IDictionary<int, IFieldDescriptor> GetBLLObjTypePrimaryKeyFieldDescriptors(Type bllObjType)
        {
            Dictionary<int, IFieldDescriptor> result = new Dictionary<int, IFieldDescriptor>();

            IDictionary<int, MemberInfo> pkmis = GetBLLObjTypePrimaryKeyMemberInfos(bllObjType);
            foreach (MemberInfo mi in pkmis.Select<KeyValuePair<int, MemberInfo>, MemberInfo>(kv => kv.Value))
            {
                if (mi is FieldInfo)
                {
                    FieldInfo fi = mi as FieldInfo;
                    string fieldName;
                    PrimaryKeyAttribute pka = (PrimaryKeyAttribute)fi.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).First();
                    if (String.IsNullOrEmpty(pka.FieldName))
                        fieldName = fi.Name;
                    else
                        fieldName = pka.FieldName;
                    if (IsResolvebleToDBMSType(fi.FieldType) == false)
                        throw new Exception("Types memeber attributed with PrimaryKeyAttribute have type that is unresolveble to dbms type.");
                    int i = pka.Ord;
                    result.Add(i, new FieldDescriptor(fi.FieldType, fieldName));
                }
                else if (mi is PropertyInfo)
                {
                    PropertyInfo pi = mi as PropertyInfo;
                    string fieldName;
                    PrimaryKeyAttribute pka = (PrimaryKeyAttribute)pi.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).First();
                    if (String.IsNullOrEmpty(pka.FieldName))
                        fieldName = pi.Name;
                    else
                        fieldName = pka.FieldName;
                    if (IsResolvebleToDBMSType(pi.PropertyType) == false)
                        throw new Exception("Types memeber attributed with PrimaryKeyAttribute have type that is unresolveble to dbms type.");
                    int i = pka.Ord;
                    result.Add(i, new FieldDescriptor(pi.PropertyType, fieldName));
                }
            }

            return result;
        }

        public IPrimaryKeyDescriptor GetBLLObjTypePrimaryKeyDescriptor(Type bllObjType)
        {
            IPrimaryKeyDescriptor result = null;

            string tableName = GetBLLObjTypeTableName(bllObjType);

            IDictionary<int, IFieldDescriptor> fds = GetBLLObjTypePrimaryKeyFieldDescriptors(bllObjType);

            string name = String.Format("{0}_PK", tableName);

            result = new PrimaryKeyDescriptor(tableName, fds, name);

            return result;
        }

        public IEnumerable<IFieldDescriptor> GetBLLObjTypeFieldDescriptors(Type bllObjType)
        {
            List<IFieldDescriptor> result = new List<IFieldDescriptor>();

            IEnumerable<FieldInfo> fieldInfos = bllObjType.GetFields(BindingFlags.Public | BindingFlags.Instance);
            fieldInfos = fieldInfos.Where(fi => fi.GetCustomAttributes(typeof(FieldAttribute), true).Count() > 0);
            foreach (FieldInfo fi in fieldInfos)
            {
                string fieldName;
                FieldAttribute fa = (FieldAttribute)fi.GetCustomAttributes(typeof(FieldAttribute), true).First();
                if (String.IsNullOrEmpty(fa.FieldName))
                    fieldName = fi.Name;
                else
                    fieldName = fa.FieldName;

                if (IsResolvebleToDBMSType(fi.FieldType) == false)
                    throw new Exception("Types memeber attributed with PrimaryKeyAttribute have type that is unresolveble to dbms type.");

                if (fi.FieldType == typeof(string) && fa.Attributes != null && fa.Attributes.Count() == 1)
                    result.Add(new FieldDescriptor(fi.FieldType, fieldName, new object[] { fa.Attributes[0] }));
                else if (fi.FieldType == typeof(decimal) && fa.Attributes != null && fa.Attributes.Count() == 2)
                    result.Add(new FieldDescriptor(fi.FieldType, fieldName, new object[] { fa.Attributes[0], fa.Attributes[1] }));
                else
                    result.Add(new FieldDescriptor(fi.FieldType, fieldName));
            }

            IEnumerable<PropertyInfo> propertyInfos = bllObjType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            propertyInfos = propertyInfos.Where(pi => pi.GetCustomAttributes(typeof(FieldAttribute), true).Count() > 0);
            foreach (PropertyInfo pi in propertyInfos)
                if (pi.CanRead && pi.CanWrite)
                {
                    string fieldName;
                    FieldAttribute fa = (FieldAttribute)pi.GetCustomAttributes(typeof(FieldAttribute), true).First();
                    if (String.IsNullOrEmpty(fa.FieldName))
                        fieldName = pi.Name;
                    else
                        fieldName = fa.FieldName;

                    if (IsResolvebleToDBMSType(pi.PropertyType) == false)
                        throw new Exception("Types memeber attributed with PrimaryKeyAttribute have type that is unresolveble to dbms type.");

                    if (pi.PropertyType == typeof(string) && fa.Attributes != null && fa.Attributes.Count() == 1)
                        result.Add(new FieldDescriptor(pi.PropertyType, fieldName, new object[] { fa.Attributes[0] }));
                    else if (pi.PropertyType == typeof(decimal) && fa.Attributes != null && fa.Attributes.Count() == 2)
                        result.Add(new FieldDescriptor(pi.PropertyType, fieldName, new object[] { fa.Attributes[0], fa.Attributes[1] }));
                    else
                        result.Add(new FieldDescriptor(pi.PropertyType, fieldName));
                }

            return result;
        }

        public IEnumerable<IForeignKeyDescriptor> GetBLLObjTypeForeignKeyDescriptors(Type bllObjType)
        {
            List<IForeignKeyDescriptor> result = new List<IForeignKeyDescriptor>();

            IEnumerable<ForeignKeyDeclarationAttribute> fkdas = 
                bllObjType.GetCustomAttributes(typeof(ForeignKeyDeclarationAttribute), true)
                    .Select<object, ForeignKeyDeclarationAttribute>(a => (ForeignKeyDeclarationAttribute)a)
                    .Where<ForeignKeyDeclarationAttribute>(fkda => fkda.RelationQuantity != RelationQuantity.ManyOrZeroToManyOrZero);
            foreach (ForeignKeyDeclarationAttribute fkda in fkdas)
            {
                string tableName = GetBLLObjTypeTableName(bllObjType);
                string relatedTableName = GetBLLObjTypeTableName(fkda.ForeignType);
                string name = String.Format("{0}_{1}_FK_{2}", tableName, relatedTableName, fkda.Ord);
                IDictionary<int, IFieldDescriptor> relatedFieldDescriptors = GetBLLObjTypePrimaryKeyFieldDescriptors(fkda.ForeignType);
                Dictionary<int, IFieldDescriptor> fieldDescriptors = new Dictionary<int, IFieldDescriptor>();
                foreach (KeyValuePair<int, IFieldDescriptor> kv in relatedFieldDescriptors)
                    if (fkda.RelationQuantity == RelationQuantity.OneOrZeroToManyOrZero || fkda.RelationQuantity == RelationQuantity.OneOrZeroToOneOrZero)
                        fieldDescriptors.Add(kv.Key, new FieldDescriptor(GetCLRNullableType(kv.Value.FieldType), String.Format("{0}_{1}_{2}_{3}_{4}", tableName, relatedTableName, ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs, fkda.Ord, kv.Key)));
                    else //if (fkda.RelationQuantity == RelationQuantity.OneToManyOrZero || fkda.RelationQuantity == RelationQuantity.OneToOneOrZero)
                        fieldDescriptors.Add(kv.Key, new FieldDescriptor(kv.Value.FieldType, String.Format("{0}_{1}_{2}_{3}_{4}", tableName, relatedTableName, ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs, fkda.Ord, kv.Key)));
                result.Add(new ForeignKeyDescriptor(tableName, fieldDescriptors, relatedTableName, relatedFieldDescriptors, name));
            }

            return result;
        }

        public IRowDescriptor GetBLLObjTypeRowDescriptor(Type bllObjType)
        {
            IRowDescriptor result = null;

            string tableName = GetBLLObjTypeTableName(bllObjType);
            IPrimaryKeyDescriptor pkd = GetBLLObjTypePrimaryKeyDescriptor(bllObjType);
            IEnumerable<IForeignKeyDescriptor> fkds = GetBLLObjTypeForeignKeyDescriptors(bllObjType);
            IEnumerable<IFieldDescriptor> fds = GetBLLObjTypeFieldDescriptors(bllObjType);

            result = new RowDescriptor(tableName, fds, fkds.Union(new List<IRelationDescriptor>() { pkd }));
            return result;
        }

        public bool CheckBLLObjTypeAndRowDescCorrespondence(Type bllObjType, IRowDescriptor rd)
        {
            //checks of correspondence of ForeignKeyDeclarationAttribute and related RowDescriptors can be added here too

            IRowDescriptor typeRowDesc = GetBLLObjTypeRowDescriptor(bllObjType);

            if (String.Compare(typeRowDesc.TableName, rd.TableName, StringComparison.InvariantCultureIgnoreCase) != 0)
                return false;

            if (!typeRowDesc.PrimaryKeyDescriptor.Equals(rd.PrimaryKeyDescriptor))
                return false;

            foreach (IForeignKeyDescriptor typefkd in typeRowDesc.ForeignKeyDescriptors)
            {
                bool fkdFound = false;
                foreach (IForeignKeyDescriptor fkd in rd.ForeignKeyDescriptors)
                    if (typefkd.Equals(fkd))
                    {
                        fkdFound = true;
                        break;
                    }
                if (fkdFound == false)
                    return false;
            }

            foreach (IFieldDescriptor typefd in typeRowDesc.FieldDescriptors)
            {
                bool fdFound = false;
                foreach (IFieldDescriptor fd in rd.FieldDescriptors)
                    if (typefd.Equals(fd))
                    {
                        fdFound = true;
                        break;
                    }
                if (fdFound == false)
                    return false;
            }

            return true;
        }


    }
}
